﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Security.Permissions;
using System.Text;
using System.Web;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.XPath;
using Com.FastSearch.SharePoint.Interfaces;
using Com.FastSearch.SharePoint.WebControls.ESPSearchWebParts.UI.Editors;

namespace Com.FastSearch.SharePoint.WebControls.ESPSearchWebParts.UI
{
    /// <summary>
    /// This WebPart displays the sorting on the result list which can be performed.
    /// In the Editor part, the fields/rank profiles which shall be displayed can be selected.
    /// <remarks>
    /// <b>Note:</b> Rank profiles do not support ascending/descending, where for the fields a default sorting can be selected.
    /// </remarks>
    ///
    /// The rendering is done using XSLT.
    ///
    /// </summary>
    public class ESPSearchAction : ESPSearchWebPart
    {
        #region Private variables

        /// <summary>
        /// xml document created for the xslt transformation
        /// </summary>
        private XmlDocument searchActionXml = null;
        
        #endregion

        /// <summary>
        /// List of SearchSortActions which have been configured in the WebPart Editor
        /// </summary>
        private List<SearchSortAction> searchActions;

        #region WebPart Properties

        /// <summary>
        /// String of the configured/selected sort fields/rank-profiles.
        /// As web parts do not allow to have custom properties other then shown in
        /// http://msdn.microsoft.com/en-us/library/ms948927.aspx a string is used to store the configured values.
        /// this string later is parsed to a List&gt;SearchSortActions&lt;.
        /// See static methods <see cref="BuildListFromString"/> and <see cref="BuildStringFromList"/> for details.
        /// </summary>
        [Category(_CATEGORY_STR), Personalizable(PersonalizationScope.Shared), WebBrowsable(false), DefaultValue("default:0:Default Ranking:True;")]
        public string SearchSortActions
        {
            get;
            set;
        }
        #endregion

        #region Web Part overrides

        /// <inheritdoc/>
        public override string GetEmbeddedXsl()
        {
            return LoadXslFromAssembly("SearchAction.xslt");
        }

        /// <summary>
        /// Configure the search.
        /// Does nothing for this WebPart as no additional search properties do have to be added to the search request.
        /// </summary>
        protected override void ConfigureSearch()
        {
            // No web part proeprties needed in the search request object..
        }

        /// <summary>
        /// Return the xpath navigator for the result xml object used by the xslt transformation.
        /// </summary>
        /// <param name="viewPath">View path for the xpath navigator. Not used as the root is returned.</param>
        /// <returns><see cref="XPathNavigator"/></returns>
        [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
        protected override XPathNavigator GetXPathNavigator(string viewPath)
        {
            if (searchActionXml != null)
            {
                return searchActionXml.CreateNavigator();
            }
            // Empty result list
            return new XmlDocument().CreateNavigator();
        }
        #endregion

        #region Custom Editor Registering

        /// <summary>
        /// Register the custom editor to be able to select the sorting fields/rank-profiles, specify more readeable
        /// display names and define default sorting directions for sorting fields. (Not for rank-profiles)
        /// </summary>
        /// <returns></returns>
        public override EditorPartCollection CreateEditorParts()
        {
            List<EditorPart> customEditorPartCollection = new List<EditorPart>();
            ESPSearchActionEditorPart configEditor = new ESPSearchActionEditorPart(SearchRequest.ESPSearchView);
            configEditor.ID = "ESPSearchActionEditorPart" + ESPSearchGroupID;
            customEditorPartCollection.Add(configEditor);
            EditorPartCollection editorPartCollection = new EditorPartCollection(customEditorPartCollection);
            return editorPartCollection;
        }

        #endregion

        #region ESPSearchWebPart Search Result callback

        /// <summary>
        /// Called from the UCF once the search has been performed.
        /// Builds up the xml object to be used for xslt transormation later.
        /// </summary>
        /// <param name="SearchResult">IESPSearchResult for the current search</param>
        public override void SetSearchResult(IESPSearchResult SearchResult)
        {
            if (SearchResult != null)
            {
                // Build the list of SearchSort Actions from the configuration string
                searchActions = BuildListFromString(SearchSortActions);
                searchActionXml = new XmlDocument();
                BuildXmlSearchAction();
            }
        }

        #endregion

        #region XML Document Building
        /// <summary>
        /// Build an ICollection&gt;SearchAction&lt; from the string which is used as web part property.
        /// This has to be done as the web parts cannot only be of a limited selection of types.
        /// See: http://msdn.microsoft.com/en-us/library/ms948927.aspx for more information.
        /// What we do is to store the sorting actions in a string instead and use this static method to build up the
        /// list of SearchAction's.
        /// We do not use XmlSerialization here as that would be an overhead in this case.
        /// </summary>
        /// <param name="searchActionsString"></param>
        /// <returns></returns>
        public static List<SearchSortAction> BuildListFromString(string searchActionsString)
        {
            List<SearchSortAction> l = new List<SearchSortAction>();
            if (!string.IsNullOrEmpty(searchActionsString))
            {
                string[] listEntries = searchActionsString.Split(';');
                foreach (string str in listEntries)
                {
                    if (!string.IsNullOrEmpty(str.Trim()))
                    {
                        string[] props = str.Split(':');

                        // The substring must have 3 entries
                        if (props.Length == 4)
                        {
                            SearchSortAction ssaction = new SearchSortAction(props[0]);
                            string directionStr = props[1];
                            int sortDir;
                            bool convertOk = Int32.TryParse(directionStr, out sortDir);
                            if (!convertOk)
                            {
                                sortDir = (int) SortOrderEnum.Ascending;
                            }
                            ssaction.DefaultSortOrder = (SortOrderEnum) sortDir;
                            if (string.IsNullOrEmpty(props[2].Trim()))
                            {
                                ssaction.DisplayName = ssaction.FieldName;
                            } else
                            {
                                ssaction.DisplayName = props[2];
                            }
                            Boolean isRankProfile;
                            convertOk = Boolean.TryParse(props[3], out isRankProfile);
                            if (!convertOk)
                            {
                                isRankProfile = true;
                            }
                            ssaction.IsRankProfile = isRankProfile;

                            l.Add(ssaction);
                        }
                    }
                }
            }
            return l;
        }

        /// <summary>
        /// Build a string which can be used to persist the web part coniguration of the selected sort fields/rank-profiles
        ///  and it's default sort direction.
        /// </summary>
        /// <param name="sortList">List of SearchSortAction's</param>
        /// <returns>String containing the information about the selected sorting.
        /// example: <br/>
        /// <![CDATA[ fieldName:0:DisplayName:True;size:1:SIZE:False ]]>
        /// SearchSortAction's are seperated by ; where as the field's are seperated by :
        /// The ordering is:
        /// FieldName: Integer of SortOrderEnum: Display Name: IsRankProfile;
        /// </returns>
        internal static string BuildStringFromList(List<SearchSortAction> sortList)
        {
            StringBuilder searchactions = new StringBuilder(100);

            foreach (SearchSortAction action in sortList)
            {
                searchactions.Append(action.FieldName);
                searchactions.Append(":");
                searchactions.Append((int)action.DefaultSortOrder);
                searchactions.Append(":");
                searchactions.Append(action.DisplayName);
                searchactions.Append(":");
                searchactions.Append(action.IsRankProfile.ToString());
                searchactions.Append(";");
            }
            return searchactions.ToString();
        }

        /// <summary>
        /// Build the Xml used for xslt transormation for the Search-Actions.
        /// To see the xml output you can change the xslt to an xml dumper as described in the Readme.txt file.
        /// </summary>
        private void BuildXmlSearchAction()
        {
            SearchSortAction selectedCustomSort = GetCustomSelectedSort();

            SortAndAddDefaultSortIfNeeded(selectedCustomSort,
                GetSortActionFromSortString(SearchRequest.ResultSorting, true));

            NavigationHelperParameter navHelperParam = new NavigationHelperParameter(Page.Request);
            XmlNode root = searchActionXml.AppendChild(searchActionXml.CreateElement("SearchActions"));
            foreach (SearchSortAction action in searchActions)
            {
                if (selectedCustomSort != null)
                {
                    if (action.FieldName.CompareTo(selectedCustomSort.FieldName) == 0)
                    {
                        action.SelectedSortOrder = selectedCustomSort.SelectedSortOrder ;
                    }
                }

                XmlElement elem =  searchActionXml.CreateElement("SortAction");

                XmlAttribute attr = searchActionXml.CreateAttribute("isSelected");
                if (string.IsNullOrEmpty(action.SelectedSortOrderString))
                {
                    attr.Value = "false";
                } else
                {
                    attr.Value = "true";
                }
                elem.Attributes.Append(attr);

                XmlElement dispName = CreateSearchActionNode(searchActionXml, "DisplayName", action.DisplayName);
                elem.AppendChild(dispName);
                XmlElement fieldName = CreateSearchActionNode(searchActionXml, "FieldName", action.FieldName);
                elem.AppendChild(fieldName);

                XmlElement sortOrder = CreateSearchActionNode(searchActionXml, "DefaultSortDirection", action.DefaultSortOrderString);
                elem.AppendChild(sortOrder);

                XmlElement selSortOrder = CreateSearchActionNode(searchActionXml, "SelectedSortDirection", action.SelectedSortOrderString);
                elem.AppendChild(selSortOrder);

                XmlElement isRankProfile = CreateSearchActionNode(searchActionXml, "IsRankProfile", action.IsRankProfile.ToString());
                elem.AppendChild(isRankProfile);

                XmlElement sortOrderElem = CreateSearchActionNode(searchActionXml, "SortActionUrl", GetSortingActionURL(
                    action,
                    navHelperParam, ESPSearchGroupID));
                elem.AppendChild(sortOrderElem);
                root.AppendChild(elem);
            }
        }

        /// <summary>
        /// Sort the sorting fields so that the one selected is always at the start.
        /// If no sorting is selected use the default sorting used for the search.
        ///
        /// Also add the default sorting as link if it does not exist.
        /// This can happen if the default sorting for the search changes and this web part has not been updated correctly.
        /// </summary>
        /// <param name="selectedCustomSort">Currently Selected Sorting</param>
        /// <param name="defaultSearchSortAction">Default Sorting</param>
        private void SortAndAddDefaultSortIfNeeded(SearchSortAction selectedCustomSort, SearchSortAction defaultSearchSortAction)
        {
            bool defaultSortFound = false;
            foreach (SearchSortAction action in searchActions)
            {
                if (selectedCustomSort != null)
                {
                    if (action.FieldName.CompareTo(selectedCustomSort.FieldName) == 0)
                    {
                        action.SelectedSortOrder = selectedCustomSort.SelectedSortOrder;
                    }
                }
                if (defaultSearchSortAction != null &&
                    action.FieldName.CompareTo(defaultSearchSortAction.FieldName) == 0)
                {
                    defaultSortFound = true;
                    // If nothing has been selected, display the default sorting as defined in the web part configuring the search
                    // (ESPCoreResults or ESPSearchConfig)
                    if (selectedCustomSort == null)
                    {
                        action.SelectedSortOrder = action.DefaultSortOrder;
                    }
                }
            }

            // Add the configured default sort if not explicitely specified in
            // the sort list (Usually configured in the Search list or search config webpart).
            // But it can happen that if the default sort changes this webpart
            // does not contain the default sort, so we add it here.
            // Also. If nothing is selected then the default sorting is show as selected
            if (!defaultSortFound && defaultSearchSortAction != null)
            {
                if (selectedCustomSort == null)
                {
                    defaultSearchSortAction.SelectedSortOrder = defaultSearchSortAction.DefaultSortOrder;
                }
                searchActions.Add(defaultSearchSortAction);
            }

            // Now sort the list to show the sorted field/rank profile first, and after the rest of the fields
            // in ascending order
            ((List<SearchSortAction>)searchActions).Sort(new SearchSortActionSorterComperator());

        }

        /// <summary>
        /// Try to find the current sorting which has been selected.
        /// It takes car of the search group this web part is in.
        /// </summary>
        /// <returns>null if the sort url property is not available, the SearchSortAction with the SelectedSortOrder property set otherwise</returns>
        private SearchSortAction GetCustomSelectedSort()
        {
            string sort = GetSafeQueryString(Client.SearchRequest.CUSTOM_RESULT_SORTING +
                Client.SearchRequest.GetPropertyPostfixForGroup(ESPSearchGroupID), null);
            SearchSortAction sortaction = GetSortActionFromSortString(sort, false);
            if (sortaction != null)
            {
                sortaction.SelectedSortOrder = sortaction.DefaultSortOrder;
            }
            return sortaction;
        }

        /// <summary>
        /// Return the SearchSortAction from an URL parameter value
        /// The values are of the sort [+|-]fieldname
        /// </summary>
        /// <param name="sort">url sort value of the type shown above</param>
        /// <param name="createDefaultIfNull">true iff the "default" rank-profile should be used instead of returning null when sort is null</param>
        /// <returns>SearchSortAction for the sort string parameter. Null if the provided sort parameter is null or empty</returns>
        internal static SearchSortAction GetSortActionFromSortString(string sort, bool createDefaultIfNull)
        {
            // If there has not default sort been defined use the "default" rankprofile.

            if (string.IsNullOrEmpty(sort))
            {
                if (createDefaultIfNull)
                {
                    sort = "default";
                }
                else
                {
                    return null;
                }
            }

            if (sort.Trim().StartsWith("+"))
            {
                SearchSortAction sortAction = new SearchSortAction(sort.Substring(1), sort.Substring(1), SortOrderEnum.Ascending);
                return sortAction;
            }
            else if (sort.Trim().StartsWith("-"))
            {
                SearchSortAction sortAction = new SearchSortAction(sort.Substring(1), sort.Substring(1), SortOrderEnum.Descending);
                 return sortAction;
            } else
            {
                SearchSortAction sortAction = new SearchSortAction(sort);
                sortAction.IsRankProfile = true;
                return sortAction;
            }
        }

        /// <summary>
        /// Return the query parameter value for a certain property name.
        /// If the property cannot be found in the uri then the defaultValue method parameter is returned.
        /// </summary>
        /// <param name="property">URI Property</param>
        /// <param name="defaultValue">Default value to be returned if the property cannot be found</param>
        /// <returns>parameter value if found in URI, the defaultValue parameter otherwise</returns>
        private string GetSafeQueryString(string property, string defaultValue)
        {
            try
            {
                return Page.Request.QueryString[property];
            }
            catch (NullReferenceException)
            {
                return defaultValue;
            }
        }

        /// <summary>
        /// Build a sorting URI for a certain sorting.
        /// It constructs the URI from the current get request and adds the sorting parameter to it.
        /// </summary>
        /// <param name="action">SearchSortAction the uri has to be build for</param>
        /// <param name="navHelperParam">NavigationHelperParameter which contains the needed information for building the URI</param>
        /// <param name="group">Search group if this web part belongs to</param>
        /// <returns>URI string for the sorting</returns>
        private static string GetSortingActionURL(SearchSortAction action,
            NavigationHelperParameter navHelperParam,
            ESPSearchGroupEnum group)
        {
            if (action.SelectedSortOrder == SortOrderEnum.None)
            {
                return NavigationHelper.SetParameter(navHelperParam, Client.SearchRequest.CUSTOM_RESULT_SORTING,
                action.DefaultSortOrderString + action.FieldName, group);
            }
            return NavigationHelper.SetParameter(navHelperParam, Client.SearchRequest.CUSTOM_RESULT_SORTING,
                SearchSortAction.InverseSortOrderString(action.SelectedSortOrder) + action.FieldName, group);
        }

        /// <summary>
        /// Create a Search Action XML Node containing the sorting information for a field/rank-profile
        /// </summary>
        /// <param name="doc">XmlDocument to build the node</param>
        /// <param name="nodeName">Name of the XML Node</param>
        /// <param name="nodeValue">Value of the XML Node</param>
        /// <returns>An XmlElement with the name of the nodeName method parameter and the value in a CData section of the method parameter nodeValue</returns>
        private static XmlElement CreateSearchActionNode(XmlDocument doc, string nodeName, string nodeValue)
        {
            XmlElement elem = doc.CreateElement(nodeName);
            XmlCDataSection data=  doc.CreateCDataSection(nodeValue);
            elem.AppendChild(data);
            return elem;
        }

        #endregion
    }

    /// <summary>
    /// Sort the sorting field by the Display Name in ascending order.
    /// Case is ignored.
    /// </summary>
    internal class SearchSortActionSorterComperator : IComparer<SearchSortAction>
    {
        public int Compare(SearchSortAction x, SearchSortAction y)
        {
            return String.Compare(x.DisplayName, y.DisplayName, true);
        }
    }

    /*
    /// <summary>
    /// Comperator which sorts the sort fields so that the selected one is the first element.
    /// The rest of the sort fields are sorted by name. (Case ignored)
    /// </summary>
    internal class SearchSortActionSorterComperator: IComparer<SearchSortAction>
    {
        public int Compare(SearchSortAction x, SearchSortAction y)
        {
            if (x.Equals(y))
            {
                return 0;
            }
            if (x.SelectedSortOrder == SortOrderEnum.Ascending ||
                x.SelectedSortOrder == SortOrderEnum.Descending)
            {
                return -1;
            }
            else if (y.SelectedSortOrder == SortOrderEnum.Ascending ||
                     y.SelectedSortOrder == SortOrderEnum.Descending)
            {
                return 1;
            }
            else
            {
                return String.Compare(x.FieldName, y.FieldName, true);
            }
        }
    }
     */



    #region Search Sort Action class and SortOrderEnum

    /// <summary>
    /// Sorting enum containg the sorting possibilites of a field/rank-profile
    /// The None enum value is used for some special cases and should be used with caution.
    /// </summary>
    public enum SortOrderEnum
    {
        /// <summary>
        /// Sort results ascending.
        /// </summary>
        Ascending,

        /// <summary>
        /// Sort results descending.
        /// </summary>
        Descending,

        /// <summary>
        /// No Sorting is used. Used for rank profiles.
        /// </summary>
        None,
    }


    /// <summary>
    /// The SearchSortAction contains information about the sorting of a index profile field/rank profile.
    /// </summary>
    public class SearchSortAction
    {
        /// <summary>
        /// Default sort order of this property.
        /// Only used for sorteable fields. Not for rank profile
        /// </summary>
        private SortOrderEnum defaultSortOrder = SortOrderEnum.Ascending;

        /// <summary>
        /// Selected ordering of this sorteable field.
        /// Only used for sorteable fields. Not for rank profile
        /// </summary>
        private SortOrderEnum selectedSortOrder = SortOrderEnum.None;

        /// <summary>
        /// string to be used in as ascending marker as prefix for the sorteable field
        /// </summary>
        private static string ASCENDING_STRING = "+";
        /// <summary>
        /// string to be used in as descending marker as prefix for the sorteable field
        /// </summary>
        private static string DESCENDING_STRING = "-";
        /// <summary>
        /// string to be used if no ordering is selected
        /// </summary>
        private static string NONE_STRING = "";

        /// <summary>
        /// sorting field/rankprofile name
        /// </summary>
        private readonly string fieldName;
        /// <summary>
        /// Name to be displayed in the UI
        /// </summary>
        private string displayName;
        /// <summary>
        /// Marker to identify if this sorting is a rank profile or a sorteable field
        /// </summary>
        private bool isRankProfile = false;

        /// <summary>
        /// SearchSortAction initializes the object with the field/rank-profile name.
        /// The display name is set equal to the rank-profile name.
        /// </summary>
        /// <param name="fieldName">name of the sorteable field/rank profile</param>
        public SearchSortAction(string fieldName)
        {
            this.fieldName = fieldName;
            displayName = fieldName;
        }

        /// <summary>
        /// SearchSortAction initializes the object with the field/rank-profile name.
        /// </summary>
        /// <param name="fieldName">name of the sorteable field/rank profile</param>
        /// <param name="displayName">name to be used to display to display to the user</param>
        /// <param name="sortOrder">The default sorting order which shall be used if the sorting is selected</param>
        public SearchSortAction(string fieldName, string displayName, SortOrderEnum sortOrder) :this(fieldName)
        {
            this.displayName = displayName;
            defaultSortOrder = sortOrder;
        }

        /// <summary>
        /// Specified if the SearchSortAction is a rank profile or a sorteable field
        /// </summary>
        public bool IsRankProfile
        {
            get { return isRankProfile; }
            set { isRankProfile = value; }
        }

        /// <summary>
        /// Field name as returned from the index-profile
        /// </summary>
        public string FieldName
        {
            get { return fieldName; }
        }

        /// <summary>
        /// Name to be used for displaying to the user.
        /// Can be configured in the <see cref="ESPSearchAction" /> web part
        /// </summary>
        public string DisplayName
        {
            get { return displayName; }
            set { displayName = value; }
        }

        /// <summary>
        /// Return the default sort order of this sorteable field.
        /// Rank profiles do not support sort ordering.
        /// </summary>
        public SortOrderEnum DefaultSortOrder
        {
            get { return defaultSortOrder; }
            set { defaultSortOrder = value;}
        }

        /// <summary>
        /// Return the string value representation of the <see cref="DefaultSortOrder"/> property
        /// </summary>
        public string DefaultSortOrderString
        {
            get
            {
                if (defaultSortOrder == SortOrderEnum.Ascending)
                {
                    return ASCENDING_STRING;
                } else
                {
                    return DESCENDING_STRING;
                }
            }
        }

        /// <summary>
        /// Return the inverted sort order string value representation of the provided method parameter.
        ///
        /// </summary>
        /// <param name="order">sort order to invert.</param>
        /// <returns> <see cref="NONE_STRING"/> if <see cref="SortOrderEnum.None"/> is passed as parameter. Otherwise Ascending will be inverted to Descending and the appropriate string repesentation is returned and vice versa</returns>
        public static string InverseSortOrderString(SortOrderEnum order)
        {
            if (order == SortOrderEnum.None)
            {
                return NONE_STRING;
            }
            return order == SortOrderEnum.Ascending ? DESCENDING_STRING : ASCENDING_STRING;
        }

        /// <summary>
        /// Property to hold the status of the sort field.
        /// </summary>
        public SortOrderEnum SelectedSortOrder
        {
            get { return selectedSortOrder; }
            set { selectedSortOrder = value; }
        }

        /// <summary>
        /// Return the string value oof the SelectedSortOrder property
        /// </summary>
        public string SelectedSortOrderString
        {
            get
            {
                if (selectedSortOrder == SortOrderEnum.Ascending)
                {
                    return ASCENDING_STRING;
                }
                else if (selectedSortOrder == SortOrderEnum.Descending)
                {
                    return DESCENDING_STRING;
                } else
                {
                    return NONE_STRING;
                }
            }
        }
    }
#endregion
}
